//
//  Chipset.hpp
//  Clock Signal
//
//  Created by Thomas Harte on 22/07/2021.
//  Copyright © 2021 Thomas Harte. All rights reserved.
//

#pragma once

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>

#include "Activity/Source.hpp"
#include "ClockReceiver/ClockingHintSource.hpp"
#include "ClockReceiver/JustInTime.hpp"
#include "Components/6526/6526.hpp"
#include "Outputs/CRT/CRT.hpp"
#include "Processors/68000/68000.hpp"
#include "Storage/Disk/Controller/DiskController.hpp"
#include "Storage/Disk/Drive.hpp"

#include "Audio.hpp"
#include "Bitplanes.hpp"
#include "Blitter.hpp"
#include "Copper.hpp"
#include "DMADevice.hpp"
#include "Flags.hpp"
#include "Keyboard.hpp"
#include "MouseJoystick.hpp"
#include "MemoryMap.hpp"
#include "Sprites.hpp"

namespace Amiga {

class Chipset: private ClockingHint::Observer {
public:
	Chipset(MemoryMap &memory_map, int input_clock_rate);

	struct Changes {
		int interrupt_level = 0;
		HalfCycles duration;

		Changes &operator += (const Changes &rhs) {
			duration += rhs.duration;
			return *this;
		}
	};

	/// Advances the stated amount of time.
	Changes run_for(HalfCycles);

	/// Advances to the end of the next available CPU slot.
	Changes run_until_after_cpu_slot();

	/// Performs the provided microcycle, which the caller guarantees to be a memory access.
	template <typename Microcycle>
	void perform(const Microcycle &cycle) {
		const uint32_t register_address = *cycle.address & ChipsetAddressMask;
		if(cycle.operation & CPU::MC68000::Operation::Read) {
			cycle.set_value16(read(register_address));
		} else {
			write(register_address, cycle.value16());
		}
	}

	/// Sets the current state of the CIA interrupt lines.
	void set_cia_interrupts(bool cia_a, bool cia_b);

	/// Provides the chipset's current interrupt level.
	int get_interrupt_level() {
		return interrupt_level_;
	}

	/// Inserts the disks provided.
	/// @returns @c true if anything was inserted; @c false otherwise.
	bool insert(const std::vector<std::shared_ptr<Storage::Disk::Disk>> &disks);

	// The standard CRT set.
	void set_scan_target(Outputs::Display::ScanTarget *scan_target);
	Outputs::Display::ScanStatus get_scaled_scan_status() const;
	void set_display_type(Outputs::Display::DisplayType);
	Outputs::Display::DisplayType get_display_type() const;

	// Activity observation.
	void set_activity_observer(Activity::Observer *observer) {
		cia_a_handler_.set_activity_observer(observer);
		disk_controller_.set_activity_observer(observer);
	}

	// Keyboard and mouse exposure.
	Keyboard &get_keyboard() {
		return keyboard_;
	}

	const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
		return joysticks_;
	}

	// Synchronisation.
	void flush();

	// Input for receiving collected bitplanes.
	void post_bitplanes(const BitplaneData &data);

	// Obtains the source of audio output.
	Outputs::Speaker::Speaker *get_speaker() {
		return audio_.get_speaker();
	}

private:
	friend class DMADeviceBase;

	// MARK: - Register read/write functions.
	uint16_t read(uint32_t address, bool allow_conversion = true);
	void write(uint32_t address, uint16_t value, bool allow_conversion = true);
	static constexpr uint32_t ChipsetAddressMask = 0x1fe;
	friend class Copper;

	// MARK: - E Clock and keyboard dividers.

	HalfCycles cia_divider_;
	HalfCycles keyboard_divider_;

	// MARK: - Interrupts.

	uint16_t interrupt_enable_ = 0;
	uint16_t interrupt_requests_ = 0;
	int interrupt_level_ = 0;

	void update_interrupts();
	void posit_interrupt(InterruptFlag::FlagT);

	// MARK: - Scheduler.

	template <bool stop_on_cpu> Changes run(HalfCycles duration = HalfCycles::max());
	template <bool stop_on_cpu> int advance_slots(int, int);
	template <int cycle, bool stop_if_cpu> bool perform_cycle();
	template <int cycle> void output();
	void output_pixels(int cycles_until_sync);
	void apply_ham(uint8_t);

	// MARK: - DMA Control, Scheduler and Blitter.

	uint16_t dma_control_ = 0;
	Blitter<false> blitter_;

	// MARK: - Sprites and collision flags.

	std::array<Sprite, 8> sprites_;
	std::array<TwoSpriteShifter, 4> sprite_shifters_;
	uint16_t collisions_ = 0, collisions_flags_= 0;

	uint32_t playfield_collision_mask_ = 0, playfield_collision_complement_ = 0;

	// MARK: - Raster position and state.

	// Definitions related to PAL/NTSC.
	// (Default values are PAL).
	int line_length_ = 227;
	int short_field_height_ = 312;
	int vertical_blank_height_ = 25;	// PAL = 25, NTSC = 20

	// Current raster position.
	int line_cycle_ = 0, y_ = 0;

	// Parameters affecting bitplane collection and output.
	uint16_t display_window_start_[2] = {0, 0};
	uint16_t display_window_stop_[2] = {0, 0};
	uint16_t fetch_window_[2] = {0, 0};

	// Ephemeral bitplane collection state.
	bool fetch_vertical_ = false;
	bool display_horizontal_ = false;
	bool did_fetch_ = false;

	int horizontal_offset_ = 0;
	enum HorizontalFetch {
		Started, WillRequestStop, StopRequested, Stopped
	} horizontal_fetch_ = HorizontalFetch::Stopped;

	// Output state.
	uint16_t border_colour_ = 0;
	bool is_border_ = true;
	int zone_duration_ = 0;
	uint16_t *pixels_ = nullptr;
	uint16_t last_colour_ = 0;	// Retained for HAM mode.
	void flush_output();

	Bitplanes bitplanes_;

	BitplaneData next_bitplanes_, previous_bitplanes_;
	bool has_next_bitplanes_ = false;

	int odd_priority_ = 0, even_priority_ = 0;
	bool even_over_odd_ = false;
	bool hold_and_modify_ = false;
	bool dual_playfields_ = false;
	bool interlace_ = false;
	bool is_long_field_ = false;

	BitplaneShifter bitplane_pixels_;

	int odd_delay_ = 0, even_delay_ = 0;
	bool is_high_res_ = false;

	// MARK: - Copper.

	Copper copper_;

	// MARK: - Audio.

	Audio audio_;

	// MARK: - Serial port.

	class SerialPort {
	public:
		void set_control(uint16_t);
		void set_data(uint16_t);
		uint16_t get_status();

	private:
//		uint16_t value = 0, reload = 0;
//		uint16_t shift = 0, receive_shift = 0;
//		uint16_t status;
	} serial_;

	// MARK: - Pixel output.

	Outputs::CRT::CRT crt_;
	uint16_t palette_[32]{};
	uint16_t swizzled_palette_[64]{};

	// MARK: - Mouse.
private:
	Mouse mouse_;

public:
	Inputs::Mouse &get_mouse() {
		return mouse_;
	}

	// MARK: - Joystick.
private:
	std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
	Joystick &joystick(size_t index) const {
		return *static_cast<Joystick *>(joysticks_[index].get());
	}

	// MARK: - CIAs.
private:
	class DiskController;

	class CIAAHandler: public MOS::MOS6526::PortHandler {
	public:
		CIAAHandler(MemoryMap &map, DiskController &controller, Mouse &mouse);
		void set_port_output(MOS::MOS6526::Port port, uint8_t value);
		uint8_t get_port_input(MOS::MOS6526::Port port);
		void set_activity_observer(Activity::Observer *observer);

		// TEMPORARY.
		// TODO: generalise mice and joysticks.
		// This is a hack. A TEMPORARY HACK.
		void set_joystick(Joystick *joystick) {
			joystick_ = joystick;
		}

	private:
		MemoryMap &map_;
		DiskController &controller_;
		Mouse &mouse_;
		Joystick *joystick_ = nullptr;
		Activity::Observer *observer_ = nullptr;
		static inline const std::string led_name = "Power";
	} cia_a_handler_;

	class CIABHandler: public MOS::MOS6526::PortHandler {
	public:
		CIABHandler(DiskController &controller);
		void set_port_output(MOS::MOS6526::Port port, uint8_t value);
		uint8_t get_port_input(MOS::MOS6526::Port);

	private:
		DiskController &controller_;
	} cia_b_handler_;

public:
	using CIAA = MOS::MOS6526::MOS6526<CIAAHandler, MOS::MOS6526::Personality::P8250>;
	using CIAB = MOS::MOS6526::MOS6526<CIABHandler, MOS::MOS6526::Personality::P8250>;

	// CIAs are provided for direct access; it's up to the caller properly
	// to distinguish relevant accesses.
	CIAA cia_a;
	CIAB cia_b;

private:
	// MARK: - Disk drives.

	class DiskDMA: public DMADevice<1> {
	public:
		using DMADevice::DMADevice;

		void set_length(uint16_t);
		void set_control(uint16_t);
		bool advance_dma();

		void enqueue(uint16_t value, bool matches_sync);

	private:
		uint16_t length_;
		bool dma_enable_ = false;
		bool write_ = false;
		uint16_t last_set_length_ = 0;
		bool sync_with_word_ = false;

		std::array<uint16_t, 4> buffer_;
		size_t buffer_read_ = 0, buffer_write_ = 0;

		enum class State {
			Inactive,
			WaitingForSync,
			Reading,
		} state_ = State::Inactive;
	} disk_;

	class DiskController: public Storage::Disk::Controller {
	public:
		DiskController(Cycles clock_rate, Chipset &chipset, DiskDMA &disk_dma, CIAB &cia);

		void set_mtr_sel_side_dir_step(uint8_t);
		uint8_t get_rdy_trk0_wpro_chng();

		void run_for(Cycles duration) {
			Storage::Disk::Controller::run_for(duration);
		}

		bool insert(const std::shared_ptr<Storage::Disk::Disk> &disk, size_t drive);
		void set_activity_observer(Activity::Observer *);

		void set_sync_word(uint16_t);
		void set_control(uint16_t);

	private:
		void process_input_bit(int value) final;
		void process_index_hole() final;

		// Implement the Amiga's drive ID shift registers
		// directly in the controller for now.
		uint32_t drive_ids_[4]{};
		uint32_t previous_select_ = 0;

		uint16_t data_ = 0;
		int bit_count_ = 0;
		uint16_t sync_word_ = 0x4489;	// TODO: confirm or deny guess.
		bool sync_with_word_ = false;

		Chipset &chipset_;
		DiskDMA &disk_dma_;
		CIAB &cia_;
	} disk_controller_;
	friend DiskController;

	void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final;
	bool disk_controller_is_sleeping_ = false;
	uint16_t paula_disk_control_ = 0;

	// MARK: - Keyboard.

	Keyboard keyboard_;
};

}
